package edu.hawaii.ics314.ical.model;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Array;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Objects;


/**
 * The Calendar class represents a serializable calendar.
 * 
 * @author 
 *
 */
public class Calendar extends CalendarComponent {
	
	private ArrayList<CalendarComponent> components = new ArrayList<CalendarComponent>();
	private Property<String> version;
	
	// DEVDOC: Only for deserialization.
	public Calendar() {
		super(new CalendarComponentInfo());
		version = null;
	}
	
	/**
	 * Constructor that creates a new <code>Calendar</code> and sets the 
	 * version and time zone of the calendar.
	 * 
	 * @param version - the version of the calendar.
	 * @param info - contains the properties of the calendar.
	 */
	public Calendar(Property<String> version, CalendarComponentInfo info) {
		super(info);
		this.version = Objects.requireNonNull(version);
		if(info.timeZone != null && !components.contains(info.timeZone.VALUE)) {
			addCalendarComponent(info.timeZone.VALUE);
		}
	}
	
	/**
	 * Returns the version of the calendar.
	 * @return The version of the calendar.
	 */
	public String getVersion() {
		return version.getValue();
	}
	
	/**
	 * Adds a calendar component to the calendar if it does
	 * not already have the component.
	 * 
	 * @param comp - the calendar component
	 */
	public void addCalendarComponent(CalendarComponent comp) {
		if(comp instanceof Calendar) {
			throw new IllegalArgumentException("calendars cannot be nested");
		}
		if(!components.contains(comp)) {
			components.add(Objects.requireNonNull(comp));
		}
		if(comp.getTimeZone() != null && !components.contains(comp.getTimeZone())) {
			components.add(comp.getTimeZone());
		}
	}
	
	/**
	 * Removes a calendar component from the calendar.
	 * 
	 * @param comp - the calendar component
	 * @return The removed calendar component.
	 */
	public boolean removeCalendarComponent(CalendarComponent comp) {
		return components.remove(Objects.requireNonNull(comp));
	}
	
	/**
	 * Returns an array of the <code>CalendarComponent</code>s that this calendar contains.
	 * 
	 * @return An array of the <code>CalendarComponent</code>s that this calendar contains.
	 */
	public CalendarComponent[] getCalendarComponents() {
		return components.toArray(new CalendarComponent[components.size()]);
	}

	/**
	 * Returns an array of the <code>CalendarComponent</code>s that this calendar contains.
	 * 
	 * @param componentType - The runtime type of <code>CalendarComponent</code>s returned.
	 * @return An array of the <code>CalendarComponent</code>s that this calendar contains.
	 */
	@SuppressWarnings("unchecked") // Dynamic instanceof check performed. 
	public <T extends CalendarComponent> T[] getCalendarComponents(Class<T> componentType) {
		Objects.requireNonNull(componentType);
		ArrayList<T> comps = new ArrayList<T>();
		for(CalendarComponent comp : components) {
			if(componentType.isInstance(comp)) { // Dynamic instanceof check.
				comps.add((T) comp);
			}
		}
		return comps.toArray((T[]) Array.newInstance(componentType, comps.size()));
	}

	@Override
	public void readObject(BufferedReader in) throws IOException, ParseException {
		String curLine = null;
		do {
			curLine = peekNextLine(in);
			if(curLine.equals("BEGIN:VCALENDAR")) {
				in.readLine(); // Commit the read and do nothing.
			} else if(curLine.equals("END:VCALENDAR")) {
				in.readLine(); // Commit the read.
				break; // Anything after end of calendar token is ignored.
			} else if(curLine.startsWith("BEGIN:VEVENT")) {
				// Do NOT commit the read.
				CalendarComponentInfo evtInfo = new CalendarComponentInfo();
				evtInfo.hostCalendar = this;
				Event calEvt = new Event(evtInfo);
				calEvt.readObject(in);
				addCalendarComponent(calEvt);
			} else if(curLine.startsWith("BEGIN:VTIMEZONE")) {
				// Do NOT commit the read.
				CalendarComponentInfo tzInfo = new CalendarComponentInfo();
				tzInfo.hostCalendar = this;
				TimeZone calTimeZone = new TimeZone(tzInfo);
				calTimeZone.readObject(in);
				addCalendarComponent(calTimeZone);
			} else {
				// Do NOT commit the read. Begin parsing standard properties.
				readProperties(in, new PropertyRecognizer() {

					@Override
					public boolean isRecognized(Property<String> property) throws ParseException {
						switch(property.getName()) {
						case "VERSION": 
							version = property;
							return true;
						default:
							return false;
						}
					}
					
				});
			}
		} while(curLine != null);
	}

	@Override
	public void writeObject(BufferedWriter out) throws IOException {
		final String CRLF = "\r\n";
		out.write("BEGIN:VCALENDAR");
		out.write(CRLF);
		out.write(version.toString());
		// Write out all time zones. These must be defined before they can be used in other calendar components.
		for(TimeZone tz : getCalendarComponents(TimeZone.class)) {
			tz.writeObject(out);
			out.write(CRLF);
		}
		// Now that we can reference the declared time zones, write out standard properties.
		writeProperties(out);
		// Write out all other calendar components.
		for(CalendarComponent comp : components) {
			if(!(comp instanceof TimeZone)) {
				comp.writeObject(out);
				out.write(CRLF);
			}
		}
		out.write("END:VCALENDAR");
	}
	
	/**
	 * Serializes this calendar to <code>out</code>.
	 * 
	 * @param out - The <code>OutputStream</code> to which this calendar will be written.
	 * 
	 * @throws IOException - if an error occurs when serializing or writing this calendar.
	 */
	public void toOutputStream(OutputStream out) throws IOException {
		Objects.requireNonNull(out);
		BufferedWriter streamWtr = new BufferedWriter(new OutputStreamWriter(out));
		writeObject(streamWtr);
		streamWtr.flush();
	}

	/**
	 * Serializes this calendar to the specified <code>outputFile</code>.
	 * 
	 * @param outputFile - the file to write to.
	 * @throws IOException if an error occurs during serialization.
	 */
	public void toFile(File outputFile) throws IOException {
		Objects.requireNonNull(outputFile);
		if(!outputFile.exists()) {
			throw new FileNotFoundException(outputFile.getAbsolutePath());
		}
		FileOutputStream out = null;
		try {
			out = new FileOutputStream(outputFile);
			toOutputStream(out);
		} finally {
			if(out != null) {
				out.close();
				out = null;
			}
		}
	}

	/**
	 * Deserializes a new calendar from the specified <code>InputStream</code>.
	 * 
	 * @param in - the <code>InputStream</code> in the <code>BufferedReader</code>.
	 * @return The created <code>Calendar</code>.
	 * 
	 * @throws IOException - if it cannot read <code>in</code>.
	 * @throws ParseException - if <code>readObject</code> cannot read the input text.
	 */
	public static Calendar fromInputStream(InputStream in) throws IOException, ParseException {
		Objects.requireNonNull(in);
		BufferedReader streamRdr = new BufferedReader(new InputStreamReader(in));
		Calendar deserialTgt = new Calendar();
		deserialTgt.readObject(streamRdr);
		return deserialTgt;
	}

	// DEVDOC: Convenience method.
	public static Calendar fromFile(File inputFile) throws IOException, ParseException {
		Objects.requireNonNull(inputFile);
		if(!inputFile.exists()) {
			throw new FileNotFoundException(inputFile.getAbsolutePath());
		}
		FileInputStream in = null;
		try {
			in = new FileInputStream(inputFile);
			return fromInputStream(in);
			
		} finally {
			if(in != null) {
				in.close();
				in = null;
			}
		}
	}

}
